/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.commons.doclet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jcoderz.commons.ArgumentMalformedException;
import org.jcoderz.commons.util.ArraysUtil;
import org.jcoderz.commons.util.IoUtil;
import org.jcoderz.commons.util.StringUtil;
import org.jcoderz.commons.util.XmlUtil;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.Doclet;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MemberDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.SourcePosition;
import com.sun.javadoc.Tag;
import com.sun.javadoc.ThrowsTag;
/**
* A generic doclet that writes out all javadoc information as XML.
*
* @author Andreas Mandel
*/
public class XmlDoclet
extends Doclet
{
/** The full qualified name of this class. */
private static final String CLASSNAME = XmlDoclet.class.getName();
/** The logger to use. */
private static final Logger logger = Logger.getLogger(CLASSNAME);
/** Static collector for doclet configuration. */
private static XmlDocletConfig sConfig = new XmlDocletConfig();
/** Concrete configuration for this xml doclet instance. */
private final XmlDocletConfig mConfig;
/** Output writer for current doclet. */
private Writer mOut = null;
private RootDoc mRootDoc;
/**
* Creates a new XmlDoclet with the given (fix) configuration.
* @param config the config to use.
*/
public XmlDoclet (XmlDocletConfig config)
{
try
{
mConfig = (XmlDocletConfig) config.clone();
}
catch (CloneNotSupportedException e)
{
throw new RuntimeException("Clone must be supported by config.", e);
}
}
/** {@inheritDoc} */
public static boolean start (RootDoc root)
{
if (logger.isLoggable(Level.FINER))
{
logger.entering(CLASSNAME, "start(RootDoc)", root);
}
boolean result = true;
try
{
final XmlDoclet handler = new XmlDoclet(sConfig);
handler.handle(root);
}
catch (Exception ex)
{
// CHECKME:
result = false;
root.printError(ex.getMessage());
throw new RuntimeException("Internal processing error.", ex);
}
if (logger.isLoggable(Level.FINER))
{
logger.exiting(CLASSNAME, "start(RootDoc)", Boolean.valueOf(result));
}
return result;
}
/** @see XmlDocletConfig#optionLength(String) */
public static int optionLength (String option)
{
if (logger.isLoggable(Level.FINER))
{
logger.entering(CLASSNAME, "optionLength(String)", option);
}
final int result = sConfig.optionLength(option);
if (logger.isLoggable(Level.FINER))
{
logger.exiting(CLASSNAME, "optionLength(String)",
new Integer(result));
}
return result;
}
public static boolean validOptions (String[][] arguments,
DocErrorReporter reporter)
{
if (logger.isLoggable(Level.FINER))
{
logger.entering(CLASSNAME,
"validOptions(String[][], DocErrorReporter)",
new Object[] {ArraysUtil.toString(arguments), reporter});
}
boolean result = true;
final Iterator i = Arrays.asList(arguments).iterator();
while (i.hasNext())
{
final String[] arg = (String[]) i.next();
if (logger.isLoggable(Level.FINER))
{
logger.finest("About to parse argument: "
+ ArraysUtil.toString(arg));
}
final String[] options = new String[arg.length - 1];
System.arraycopy(arg, 1, options, 0, options.length);
try
{
sConfig.parseOption(arg[0], options);
}
catch (ArgumentMalformedException ex)
{
reporter.printError(ex.getMessage());
result = false;
}
}
if (logger.isLoggable(Level.FINER))
{
logger.exiting(CLASSNAME,
"validOptions(String[][], DocErrorReporter)",
Boolean.valueOf(result));
}
return result;
}
private void handle (RootDoc root)
throws IOException
{
// create XML file
FileOutputStream streamOut = null;
mRootDoc = root;
try
{
streamOut = new FileOutputStream(
new File(mConfig.getOutputDirectory(), "javadoc.xml"), false);
mOut = new OutputStreamWriter(streamOut, "utf-8");
mOut.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
mOut.write("<javadoc>\n");
final Set handledPackages = new HashSet();
final PackageDoc[] packages = root.specifiedPackages();
for (int i = 0; i < packages.length; ++i)
{
generatePackageElement(packages[i]);
handledPackages.add(packages[i].name());
}
final ClassDoc[] classes = root.specifiedClasses();
for (int i = 0; i < classes.length; ++i)
{
if (!handledPackages.contains(
classes[i].containingPackage().name()))
{
generatePackageElement(classes[i].containingPackage());
handledPackages.add(classes[i].containingPackage().name());
}
}
mOut.write("</javadoc>");
}
finally
{
IoUtil.close(mOut);
IoUtil.close(streamOut);
mOut = null;
}
}
private void generatePackageElement (PackageDoc pkg)
throws IOException
{
mOut.write("<package");
generateDocHeader(pkg);
mOut.write(">");
generateDocBody(pkg);
final ClassDoc[] classes = pkg.allClasses();
for (int i = 0; i < classes.length; ++i)
{
generateClassElement(classes[i]);
}
mOut.write("</package>");
}
private void generateClassElement (ClassDoc cd)
throws IOException
{
mOut.write("<class");
generateClassHeader(cd);
mOut.write(">\n");
generateClassBody(cd);
mOut.write("</class>\n");
}
private void generateClassBody (ClassDoc cd)
throws IOException
{
generateProgramElementBody(cd);
// Interfaces
final ClassDoc[] interfaces = cd.interfaces();
for (int i = 0; i < interfaces.length; ++i)
{
mOut.write("<interface");
addAttribute("name", interfaces[i].qualifiedName());
mOut.write("/>\n");
}
// InnerClasses
final ClassDoc[] innerClass = cd.innerClasses();
for (int i = 0; i < innerClass.length; ++i)
{
mOut.write("<inner");
addAttribute("name", innerClass[i].qualifiedName());
mOut.write("/>\n");
}
// Fields
final FieldDoc[] fields = cd.fields();
for (int i = 0; i < fields.length; ++i)
{
final FieldDoc field = fields[i];
generateFieldHeader(field);
generateFieldBody(field);
mOut.write("</field>\n");
}
// Constructors
final ConstructorDoc[] constructors = cd.constructors();
for (int i = 0; i < constructors.length; ++i)
{
final ConstructorDoc constructor = constructors[i];
generateConstructorHeader(constructor);
generateConstructorBody(constructor);
mOut.write("</constructor>\n");
}
// Methods.
final MethodDoc[] methods = cd.methods();
for (int i = 0; i < methods.length; ++i)
{
final MethodDoc method = methods[i];
generateMethodHeader(method);
generateMethodBody(method);
mOut.write("</method>\n");
}
}
private void generateMethodHeader (MethodDoc method)
throws IOException
{
mOut.write("<method");
addAttribute("abstract", method.isAbstract());
if (method.overriddenClass() != null)
{
addAttribute("overridden-class",
method.overriddenClass().qualifiedName());
}
if (method.overriddenMethod() != null)
{
addAttribute("overridden-method",
method.overriddenMethod().qualifiedName());
}
generateExecutableMemberHeader(method);
mOut.write(">\n");
}
private void generateMethodBody (MethodDoc method)
throws IOException
{
if (method.returnType() != null
&& !method.returnType().qualifiedTypeName().equals("void"))
{
mOut.write("<return");
addAttribute("type",
method.returnType().qualifiedTypeName()
+ method.returnType().dimension());
mOut.write(">");
final Tag[] returnTag = method.tags("return");
if (returnTag != null && returnTag.length > 0)
{
generatedTagedTextElement(returnTag[0].inlineTags());
}
mOut.write("</return>\n");
}
generateExecutableMemberDocBody(method);
}
private void generatedTagedTextElement (final Tag[] tags)
throws IOException
{
mOut.write("<doc>");
generateTagedDoc(tags);
mOut.write("</doc>\n");
}
private void generateConstructorBody (ConstructorDoc constructor)
throws IOException
{
generateExecutableMemberDocBody(constructor);
}
private void generateExecutableMemberDocBody (
ExecutableMemberDoc executableMemberDoc)
throws IOException
{
generateMemberBody(executableMemberDoc);
// Parameter
final ParamTag[] tags = executableMemberDoc.paramTags();
final Map parameterTags = new HashMap();
for (int i = 0; i < tags.length; ++i)
{
if (parameterTags.containsKey(tags[i].parameterName()))
{
warn(tags[i].position(),
"Duplicate parameter ignored. " + tags[i],
executableMemberDoc);
}
else
{
// ONLY on tag per parametername
parameterTags.put(tags[i].parameterName(), tags[i]);
}
}
final Parameter[] parameters = executableMemberDoc.parameters();
for (int i = 0; i < parameters.length; ++i)
{
final ParamTag parameterTag
= (ParamTag) parameterTags.get(parameters[i].name());
// if (parameterTag == null)
// {
// warn("No tag for parameter " + parameters[i].hashCode(),
// executableMemberDoc);
// }
generateParameterHeader(parameters[i], parameterTag);
generateParameterBody(parameters[i], parameterTag);
mOut.write("</parameter>\n");
parameterTags.remove(parameters[i].name());
}
if (!parameterTags.isEmpty())
{
warn(executableMemberDoc.position(),
"Unused parameter tags: " + parameterTags, executableMemberDoc);
}
// Exceptions
final ThrowsTag[] tTags = executableMemberDoc.throwsTags();
final Map throwsTags = new HashMap();
for (int i = 0; i < tTags.length; ++i)
{
if (throwsTags.containsKey(tTags[i].exceptionName()))
{
warn(tTags[i].position(),
"Duplicate throws Tags ignored." + tTags[i].exceptionName(),
executableMemberDoc);
}
else
{
throwsTags.put(tTags[i].exceptionName(), tTags[i]);
}
}
final ClassDoc[] exceptions = executableMemberDoc.thrownExceptions();
for (int i = 0; i < exceptions.length; ++i)
{
ThrowsTag exceptionTag
= (ThrowsTag) throwsTags.remove(exceptions[i].qualifiedName());
if (exceptionTag == null)
{
exceptionTag = (ThrowsTag) throwsTags.remove(exceptions[i].name());
}
if (exceptionTag == null)
{
// warn(executableMemberDoc.position(),
// "No tag for exception " + exceptions[i].name(),
// executableMemberDoc);
}
generateThrowsHeader(exceptions[i], exceptionTag);
generateThrowsBody(exceptions[i], exceptionTag);
mOut.write("</throws>\n");
}
if (!throwsTags.isEmpty())
{
warn(executableMemberDoc.position(),
"Unused throws tags: " + throwsTags, executableMemberDoc);
}
}
private void generateMemberBody (MemberDoc member)
throws IOException
{
generateProgramElementBody(member);
}
private void generateProgramElementBody (ProgramElementDoc programElement)
throws IOException
{
generateDocBody(programElement);
}
private void generateThrowsHeader (ClassDoc parameter,
ThrowsTag exceptionTag)
throws IOException
{
mOut.write("<throws");
addAttribute("type", parameter.qualifiedName());
generateTagHeader(exceptionTag);
mOut.write(">");
}
private void generateThrowsBody (ClassDoc parameter,
ThrowsTag exceptionTag)
throws IOException
{
if (exceptionTag != null)
{
generateTagBody(exceptionTag);
}
}
private void generateParameterBody (Parameter parameter,
ParamTag parameterTag)
throws IOException
{
if (parameterTag != null)
{
generateTagBody(parameterTag);
}
}
private void generateParameterHeader (Parameter parameter,
ParamTag parameterTag)
throws IOException
{
mOut.write("<parameter");
addAttribute("name", parameter.name());
addAttribute("type",
parameter.type().qualifiedTypeName()
+ parameter.type().dimension());
mOut.write(">");
}
private void generateConstructorHeader (ConstructorDoc cd)
throws IOException
{
mOut.write("<constructor");
generateExecutableMemberHeader(cd);
mOut.write(">");
}
private void generateExecutableMemberHeader (ExecutableMemberDoc emd)
throws IOException
{
addAttribute("flat-signature", emd.flatSignature());
addAttribute("signature", emd.signature());
addAttribute("native", emd.isNative());
addAttribute("synchronized", emd.isSynchronized());
generateMemberHeader(emd);
}
private void generateMemberHeader (MemberDoc md)
throws IOException
{
addAttribute("synthetic", md.isSynthetic());
generateProgramElementHeader(md);
}
private void generateFieldBody (FieldDoc fd)
throws IOException
{
generateMemberBody(fd);
// TODO: cd.serialFieldTags()
}
private void generateFieldHeader (FieldDoc cd)
throws IOException
{
mOut.write("<field");
addAttribute("type",
cd.type().qualifiedTypeName() + cd.type().dimension());
final String constant = cd.constantValueExpression();
if (constant != null)
{
addAttribute("constant", true);
addAttribute("value", constant);
}
addAttribute("volatile", cd.isVolatile());
addAttribute("transient", cd.isTransient());
generateMemberHeader(cd);
mOut.write(">");
}
private void generateDocBody (Doc doc)
throws IOException
{
mOut.write("<doc>");
generateTagedDoc(doc.inlineTags());
mOut.write("</doc>");
final Tag[] tags = doc.tags();
for (int i = 0; i < tags.length; i++)
{
final Tag tag = tags[i];
// these tags are handled directly
if (!tag.kind().equals("@param")
&& !tag.kind().equals("@return")
&& !tag.kind().equals("@throws"))
{
generateTagElement(tag);
}
}
}
private void generateTagHeader (Tag tag)
throws IOException
{
if (tag != null)
{
if (tag.position() != null)
{
generateSourcePositionHeader(tag.position());
}
addAttribute("name", tag.name());
if (tag instanceof SeeTag)
{
final SeeTag seeTag = (SeeTag) tag;
addAttribute("label", seeTag.label());
addAttribute("referenced-class", seeTag.referencedClassName());
addAttribute("referenced-member", seeTag.referencedMemberName());
if (seeTag.referencedPackage() != null)
{
addAttribute("referenced-package",
seeTag.referencedPackage().name());
}
}
else
{
addAttribute("kind", tag.kind());
}
}
}
private void generateTagElement (Tag tag)
throws IOException
{
if (tag instanceof SeeTag)
{
generateSeeElement((SeeTag) tag);
}
else if (tag != null)
{
mOut.write("<tag");
generateTagHeader(tag);
mOut.write(">");
generateTagBody(tag);
mOut.write("</tag>");
}
}
private void generateTagBody (Tag tag)
throws IOException
{
mOut.write("<doc>");
generateTagedDoc(tag.inlineTags());
mOut.write("</doc>");
}
private void generateTagedDoc (Tag[] tags)
throws IOException
{
// build string to be tidied up
final StringBuffer data = new StringBuffer();
for (int i = 0; i < tags.length; i++)
{
final Tag inlineTag = tags[i];
if (inlineTag instanceof SeeTag)
{
data.append("<a id='SEE_TAG_");
data.append(Integer.toString(i));
data.append("'></a>");
}
else
{
data.append(inlineTag.text());
}
}
final String input = data.toString();
final String clean;
if (input.indexOf('<') != -1 || input.indexOf('&') != -1)
{
final HtmlCleaner cleaner = new HtmlCleaner();
clean = cleaner.clean(data);
final String warnings = cleaner.getWarnings();
if (!StringUtil.isEmptyOrNull(warnings))
{
mRootDoc.printWarning("Input:" + input);
mRootDoc.printWarning("Clean:" + clean);
if (cleaner.hasErrors())
{
mRootDoc.printWarning(tags[0].position(), warnings);
// mRootDoc.printError(tags[0].position(), warnings);
}
else
{
mRootDoc.printWarning(tags[0].position(), warnings);
}
}
}
else
{
clean = input;
}
data.setLength(0);
data.append(clean);
// now replace the a tag back...
int pos;
while ((pos = data.indexOf("<a id='SEE_TAG_")) != -1)
{
mOut.write(data.substring(0, pos));
final int i = Integer.parseInt(data.substring(pos
+ "<a id='SEE_TAG_".length(), data.indexOf("'></a>", pos)));
generateSeeElement((SeeTag) tags[i]);
data.delete(0, data.indexOf("'></a>", pos) + "'></a>".length());
}
mOut.write(data.toString());
}
private void generateSeeElement (SeeTag tag)
throws IOException
{
mOut.write("<see ");
generateTagHeader(tag);
mOut.write(">");
final String label;
if (!StringUtil.isNullOrEmpty(tag.label()))
{
label = tag.label();
}
else if (tag.referencedClassName() != null
&& tag.referencedMemberName() != null)
{
label = tag.referencedClassName() + "#" + tag.referencedMemberName();
}
else if (tag.referencedClassName() != null)
{
label = tag.referencedClassName();
}
else if (tag.referencedPackage() != null)
{
label = tag.referencedPackage().name();
}
else if (tag.referencedMemberName() != null)
{
label = tag.referencedMemberName();
}
else
{
label = "";
}
mOut.write(XmlUtil.escape(label));
mOut.write("</see>");
}
private void generateSourcePositionHeader (SourcePosition position)
throws IOException
{
if (position.file() != null)
{
addAttribute("file", position.file().getPath());
}
addAttribute("line", position.line());
addAttribute("column", position.column());
}
private void generateClassHeader (ClassDoc cd)
throws IOException
{
generateProgramElementHeader(cd);
addAttribute("abstract", cd.isAbstract());
addAttribute("externalizable", cd.isExternalizable());
addAttribute("serializable", cd.isSerializable());
if (cd.superclass() != null)
{
addAttribute("superclass", cd.superclass().qualifiedName());
}
}
private void generateProgramElementHeader (ProgramElementDoc pe)
throws IOException
{
addAttribute("qualified-name", pe.qualifiedName());
if (pe.containingClass() != null)
{
addAttribute("containing-class",
pe.containingClass().qualifiedName());
}
final String visibility;
if (pe.isPrivate())
{
visibility = "private";
}
else if (pe.isProtected())
{
visibility = "protected";
}
else if (pe.isPackagePrivate())
{
visibility = "package private";
}
else if (pe.isPublic())
{
visibility = "public";
}
else
{
throw new RuntimeException("Could not detect access right " + pe);
}
addAttribute("visibility", visibility);
addAttribute("static", pe.isStatic());
addAttribute("final", pe.isFinal());
addAttribute("modifiers", pe.modifiers());
addAttribute("modifier-specifier",
String.valueOf(pe.modifierSpecifier()));
generateDocHeader(pe);
}
private void generateDocHeader (Doc doc)
throws IOException
{
addAttribute("name", doc.name());
addAttribute("exception", doc.isException());
addAttribute("error", doc.isError());
addAttribute("ordinary-class", doc.isOrdinaryClass());
if (doc.position() != null)
{
generateSourcePositionHeader(doc.position());
}
if (doc.isInterface())
{
addAttribute("type", "interface");
}
else if (doc.isClass())
{
addAttribute("type", "class");
}
}
private void addAttribute (String name, boolean value)
throws IOException
{
if (value)
{
mOut.write(" ");
mOut.write(name);
mOut.write("='");
mOut.write(name);
mOut.write("'");
}
}
private void addAttribute (String name, int value)
throws IOException
{
addAttribute(name, Integer.toString(value));
}
private void addAttribute (String name, String value)
throws IOException
{
if (!StringUtil.isEmptyOrNull(value))
{
mOut.write(" ");
mOut.write(name);
mOut.write("='");
mOut.write(XmlUtil.attributeEscape(value));
mOut.write("'");
}
}
private void warn (SourcePosition pos, String string, Object parameter)
{
mRootDoc.printWarning(pos, string + " at " + parameter);
}
}